Reconociendo el Canto de las Aves: Entrenando un Modelo de Machine Learning con Espectrogramas para Identificar Especies Aviarias#

Resumen#

La identificación de aves desempeña un papel fundamental en la conservación de la biodiversidad y la conexión de las personas con la naturaleza. La participación de la comunidad no científica en proyectos de conservación ha aumentado, gracias al aprendizaje automático, que permite a personas sin experiencia contribuir con datos precisos.

Las Redes Neuronales Convolucionales (CNN) son esenciales en esta tarea, ya que procesan imágenes de espectrogramas. Estos espectrogramas visualizan la distribución de frecuencias en los sonidos de aves, revelando detalles importantes no visibles o audibles para un humano. La importancia de los sonidos de las aves, recae en que muchas veces las aves no se logran orbservar directamente, pero siempre pueden escucharse sus cantos.

El proyecto se basa en espectrogramas de 152 especies de aves en Hawaii, previamente preprocesadas. Debido a limitaciones en el equilibrio de datos, se seleccionaron las 6 especies con más datos. Se implementaron algoritmos de aprendizaje automático, como CNN y aprendizaje por transferencia con MobileNetV2, para desarrollar un modelo de identificación de aves a través de los espectrogramas de sus cantos.

Marco Teórico#

Las Redes Neuronales Convolucionales (CNN) son un tipo de arquitectura de redes neuronales profundas diseñadas específicamente para el procesamiento de datos bidimensionales, como imágenes y vídeos. Han demostrado un rendimiento sobresaliente en una amplia variedad de tareas relacionadas con la visión por computadora. Estas redes revolucionaron el procesamiento de datos al aprender directamente las características relevantes de los datos durante el entrenamiento, en lugar de depender de la extracción manual de características. El nombre “convolucional” proviene de la operación central en la primera capa de estas redes, la convolución. Este enfoque demostró su éxito inicial en tareas como el procesamiento de imágenes y datos similares. [Rubio, 2023]

  • Necesidad de las convoluciones:

En muchas aplicaciones, trabajar directamente con los datos en bruto hace que la tarea sea sencillamente inmanejable. Tomemos el ejemplo de una imagen de matriz 256x256 píxeles, tendríamos un vector de entrada de 65.536 dimensiones. Esto significa que habría aproximadamente 65 millones de parámetros que conectarían cada píxel de entrada con cada nodo de la primera capa, lo que resultaría en una red extremadamente grande y difícil de entrenar.

La complejidad aumenta aún más con imágenes de alta resolución, como 1000x1000 píxeles, y aún más si se trata de imágenes en color con una representación RGB, ya que cada píxel tendría tres valores (rojo, verde y azul), lo que triplicaría la dimensionalidad de la entrada.

_images/ffff.jpg

Fig. 2 Estructura de una imagen#

Además, a medida que se añaden capas ocultas, el número de parámetros sigue aumentando. Mientras que al emplear convoluciones, se pueden abordar simultáneamente ambos problemas, es decir, el de la explosión de parámetros y el de la extracción de información estadística útil.

Características#

  • Convolución: Se realiza en la primera capa de estas redes en lugar del producto interno (también conocido como multiplicación de matrices) que se usaba en las redes neuronales totalmente conectadas o feedforward, anteriormente.

  • No solo aprenden los parámetros de la red (los pesos y sesgos de las neuronas) durante el entrenamiento, sino que también aprenden las características directamente de los datos (La red es capaz de identificar automáticamente patrones y características relevantes en los datos sin necesidad de una etapa de preprocesamiento intensiva).

Pasos básicos de una red convolucional#

Los pasos básicos de cualquier red convolucional son:

  • Etapa de convolución

  • El paso de no linealidad

  • El paso de agrupación

Etapa de convolución#

_images/convolucion_1.png

Fig. 3 Convolución: Reparto de pesos#

La operación de convolución entre dos matrices H R m × m e I R l × l , es otra matriz definida por:

_images/formula.png

El resultado anterior se obtiene si colocamos la matriz H de (2 x 2) sobre I , empezando por la esquina superior izquierda. Desde un punto de vista físico, el valor O ( 1 , 1 ) resultante es una media ponderada sobre un área local dentro de la matriz I

\[ \begin{align}\begin{aligned}O(1,1)=h(1,1)I(1,1)+h(1,2)I(1,2)+h(2,1)I(2,1)+h(2,2)I(2,2)\\O(1,2)=h(1,1)I(1,2)+h(1,2)I(1,3)+h(2,1)I(2,2)+h(2,2)I(2,3)\\O(2,1)=h(1,1)I(2,1)+h(1,3)I(2,2)+h(2,1)I(3,1)+h(2,2)I(3,2)\\O(2,2)=h(1,1)I(2,2)+h(1,3)I(2,3)+h(2,1)I(3,2)+h(2,2)I(3,3) \end{aligned}\end{align} \]

La operación anterior se conoce como operación de correlación cruzada: operaciones ponderadas sobre los píxeles dentro de un área de ventana de una imagen. se tiene que:

  • En otras palabras, O ( i , j ) contiene información en un área de ventana de la matriz de entrada.

  • El elemento I ( i , j ) es el elemento superior izquierdo de esta área de la ventana.

  • El tamaño de la matriz de salida depende de las suposiciones que se adopten sobre cómo tratar los elementos/píxeles en los bordes de I

  • El tamaño de la ventana depende del valor de m

  • Ejemplo:

_images/w.png

Fig. 4 Ejemplo de convolución#

Estas operaciones de filtrado se han utilizado tradicionalmente para generar características a partir de imágenes. La diferencia era que los elementos de la matriz de filtrado se preseleccionaban. [Rubio, 2023]

Hide code cell source
from skimage.color import rgb2gray
#Esta función se utiliza para convertir imágenes en color en imágenes en escala de grises.
from skimage.io import imread
# Importa la función imread de la biblioteca scikit-image. Esta función se utiliza para cargar imágenes desde archivos en diversos formatos.
import numpy as np
from scipy import signal
#biblioteca científica que proporciona herramientas para diversas aplicaciones numéricas, incluyendo procesamiento de señales.
import matplotlib.pylab as pylab
#módulo que combina características de NumPy y Matplotlib para una experiencia de usuario similar a la de MATLAB.

# La conversión a tipo flotante puede ser útil si se planea realizar cálculos numéricos o procesamiento de imagen que requieran valores en coma flotante.
im = rgb2gray(imread('C:/Users/kaes1/Desktop/MachineLearningUN/imagenes/fotografo.jpeg')).astype(float)

#Esto imprime el valor máximo dentro de la matriz im, que representa el valor de intensidad más alto presente en la imagen.
# #Si la imagen está en escala de grises, este valor generalmente estará en el rango de 0 a 255,
# #donde 0 representa el negro y 255 representa el blanco en una representación de 8 bits
print(im.shape, np.max(im))
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 1
----> 1 from skimage.color import rgb2gray
      2 #Esta función se utiliza para convertir imágenes en color en imágenes en escala de grises.
      3 from skimage.io import imread

ModuleNotFoundError: No module named 'skimage'
Hide code cell source
#Este tipo de kernel se utiliza comúnmente para aplicar un filtro de promedio o desenfoque en una imagen
blur_box_kernel = np.ones((3,3)) / 9
blur_box_kernel
array([[0.11111111, 0.11111111, 0.11111111],
       [0.11111111, 0.11111111, 0.11111111],
       [0.11111111, 0.11111111, 0.11111111]])
Hide code cell source
edge_laplace_kernel = np.array([[0,1,0],[1,-4,1],[0,1,0]])
edge_laplace_kernel
array([[ 0,  1,  0],
       [ 1, -4,  1],
       [ 0,  1,  0]])
Hide code cell source
#aplica una convolución entre la imagen im y el kernel blur_box_kernel
im_blurred = signal.convolve2d(im, blur_box_kernel)

#aplica una convolución entre la imagen im y el kernel edge_laplace_kernel
im_edges = np.clip(signal.convolve2d(im, edge_laplace_kernel), 0, 1)
Hide code cell source
fig, axes = pylab.subplots(ncols=3, sharex=True, sharey=True, figsize=(18, 6))
axes[0].imshow(im, cmap=pylab.cm.gray)
axes[0].set_title('Original Image', size=20)
axes[1].imshow(im_blurred, cmap=pylab.cm.gray)
axes[1].set_title('Box Blur', size=20)
axes[2].imshow(im_edges, cmap=pylab.cm.gray)
axes[2].set_title('Laplace Edge Detection', size=20)
for ax in axes:
    ax.axis('off')
_images/9370cbafbc062455f8b9db7fd3c9d36acfa6629e639044cf95fa3d127cd6debe.png
_images/yu.png

Fig. 5 Ilustración de tres filtros/núcleos#

  • Profundidad: La profundidad de una capa es el número de matrices de filtro que se emplean en esta capa. No debe confundirse profundidad de la red, que corresponde al número total de capas ocultas utilizadas. A veces, nos referimos al número de filtros como el número de canales.

  • Campo receptivo: Cada píxel de una matriz de características de salida resulta como una media ponderada de los píxeles dentro de un área específica de la matriz de imágenes de entrada (o de la salida de la capa anterior). El área específica que corresponde a un píxel se conoce como su campo receptivo.

  • Deslizamiento: En la práctica, en lugar de deslizar la matriz de filtros de píxel en píxel, se puede deslizar, por ejemplo, s píxeles. Este valor se conoce como stride. Para valores de s > 1 , se obtienen matrices de mapas de características de menor tamaño.

  • Relleno de ceros: A veces, se utilizan ceros para rellenar la matriz de entrada alrededor de los píxeles del borde. De esta forma, la dimensión de la matriz aumenta. Si la matriz original tiene dimensiones.

  • Se puede ajustar el tamaño de una matriz de mapa de características de salida ajustando el valor del stride, s , y el número de columnas y filas cero adicionales en el relleno. En general, se puede comprobar fácilmente que si H R m × m e I R l × l y p es el número de filas y columnas adicionales para el relleno, entonces el mapa de características tiene dimensiones k x k , donde:

\[k=\left \lfloor \frac{l+2p-m}{s} +1 \right \rfloor\]
\[\left \lfloor . \right \rfloor\]

es el operador suelo.

El paso de la no linealidad#

Una vez que se han realizado las convoluciones y se ha añadido el término de sesgo a todos los valores del mapa de características, el siguiente paso es aplicar una no linealidad (función de activación) a cada uno de los píxeles de cada matriz de mapas de características. Actualmente, la función de activación lineal rectificada, ReLU, parece ser la más popular. [Rubio, 2023]

_images/nolineal.png

Fig. 6 No linealidad#

La etapa de agrupación#

El objetivo de este paso es reducir la dimensionalidad de cada matriz de mapas de características. A veces, el paso también se denomina pooling espacial. Para ello, se define una ventana y se desliza sobre la matriz correspondiente. El deslizamiento puede realizarse adoptando un valor para el respectivo parámetro stride, s . La operación de pooling consiste en elegir un único valor para representar todos los píxeles que se encuentran dentro de la ventana. La operación más utilizada es la agrupación máxima; es decir, entre todos los píxeles que se encuentran dentro de la ventana, el que tiene el valor más alto es seleccionado. Otra posibilidad es la agrupación en la que se selecciona el valor medio de todos los píxeles; a veces se denomina pooling de suma. [Rubio, 2023]

_images/pooling.png

Fig. 7 Agrupamiento#

La siguiente figura muestra el efecto de aplicar el pooling a la imagen de la izquierda. Sin duda, los bordes se vuelven más gruesos, pero la información relacionada con los bordes puede extraerse. Nótese que después de la agrupación, el tamaño de la matriz imagen es reducido. Desde otro punto de vista, el polling resume las estadísticas dentro del área pooling. El pooling puede considerarse un tipo especial de filtrado, en el que, en lugar de la convolución, se selecciona el valor máximo (o medio) de la imagen. El pooling ayuda a que la representación sea aproximadamente invariante a pequeñas traslaciones de la entrada.

_images/pooling_invariant.png

Fig. 8 Agrupamiento invariante#

Convolución sobre volúmenes#

_images/cnnn.png

Fig. 9 CNN#

Supongamos que la entrada es un volumen de I de l × l × d . Nótese que, éste comprende d imágenes, digamos, I r , r = 1 , 2 , , d cada una de ellas de dimensiones l × l Sea H el filtro volumen de m × m × d . Este último comprende el conjunto de d imágenes, H r , r = 1 , 2 , , d , cada una de dimensiones m × m . A continuación, la operación de convolución se define mediante los siguientes pasos:

  • Convolucionar las correspondientes matrices de imágenes bidimensionales para generar d matrices bidimensionales de salida, es decir:

O r = I r H r , r = 1 , 2 , , d .
  • La convolución de los dos volúmenes, I y H se define como:

O = r = 1 d O r .

En otras palabras, al convolución de dos volúmenes da como resultado una matriz bidimensional

3D volume 3D volume = 2D array .

Arquitectura CNN completa#

_images/cnncom.png

Fig. 10 Arquitectura de una CNN completa#

En la primera capa se emplea un número de volúmenes de filtro (canales) para realizar convoluciones seguidas de la operación no lineal. A continuación, la etapa de pooling toma el relevo para reducir la altura y la anchura de cada volumen de salida, que se utiliza como entrada de la segunda capa, y así sucesivamente. Por último, el volumen de salida de la última capa se vectoriza. A veces, esto también se denomina operación de aplanamiento (flattening: En otras palabras, todos los elementos del volumen de salida se apilan uno debajo de otro para formar un vector) [Rubio, 2023]

Metodologia#

Descripción de población y muestra#

Para el desarrollo del presente proyecto, se eligió un conjunto de datos que contiene imágenes de espectrogramas previamente procesados, disponibles en el sitio web Kaggle [Naqv, 2023]. Los audios utilizados para generar los espectrogramas, fueron obtenidos del sitio web Xeno-Canto, en el cual se encuentran registros de sonidos de toda clase de fauna alrededor del mundo. Los audios con los cantos de las aves fueron convertidos en espectrogramas utilizando una transformada de Fourier de tamaño 2048 y luego se les aplicó una transformación logarítmica.

Los espectrogramas consisten en una representación visual que muestra cómo se distribuyen las frecuencias en una señal de sonido. Esta representación gráfica puede revelar detalles específicos, como frecuencias elevadas o cambios en la amplitud, que podrían no ser perceptibles incluso si se encuentran dentro del rango auditivo humano [Martínez Mascorro, 2013].

La muestra inicial de datos para este proyecto son espectrogramas de 152 especies de aves de la población total de especies de aves presentes en Hawaii, y no se tiene información temporal con respecto al momento en que se registraron los audios.

Durante la realización del análisis exploratorio, y por limitaciones para aplicar técnicas de balanceo de datos, se seleccionan como muestra las 6 especies con mayor cantidad de datos.

_images/img_esp.png

Fig. 11 Especie Loxops caeruleirostris (Akeke) junto a su espectograma#

Diccionario de variables#

Los datos utilizados en el proyecto son imágenes de espectrogramas de 152 especies de aves presentes en Hawái. Como se verá más adelante, la cantidad de espectrogramas para cada especie no es igual, por lo que estamos ante un conjunto de datos desbalanceado.

  • Variable dependiente: Etiquetas con la abreviatura del nombre común de las aves.

  • Variables explicativas: Vector de características extraídas de imágenes de espectrogramas del espectro de frecuencias de la emisión sonora del canto de las aves.

Los espectogramas, se utilizaron para extraer patrones visuales y características importantes presentes en las imágenes, que permitan identificar las especies de aves. Estas caracteristicas son extraidas al momento de implementar los modelos, y no se tienen de antemano.

Algunas de las caracteristicas extraíadas de los espectrogramas por los modelos son:

  • Estadísticas de color: Los estadísticos de distribución de color son comunes en la recuperación de imágenes, ya que describen la variación de la intensidad del color en una imagen. En el caso de los espectrogramas de sonido, estos estadísticos se aplican a cada imagen monocroma, permitiendo describir cómo varía la intensidad del sonido en regiones definidas en términos de tiempo y frecuencia. [Dennis et al., 2011]

  • Direccionalidad: La direccionalidad de la imagen es una característica importante para describir la textura de una imagen. Describe la dirección en la que se concentra o dispersa la textura de la imagen. [Shi et al., 2015]

Técnicas#

Para el desarrollo de este modelo se implementaron los siguientes algoritmos de aprendizaje automatico:

  • Convolutional Neural Network (CNN)

  • Aprendizaje por trasnferencia con MobileNetV2

  • Convolutional Neural Network con Algoritmo Genetico (GA)

Diseño general#

El tipo de diseño de investigación utilizado en este trabajo es Mineria de datos. Las etapas consideradas durante el desarrollo del trabajo fueron:

  • Extracción de datos

  • Análisis exploratorio de los datos

  • Procesamiento de datos

  • Modelado

  • Evaluación de resultados

Resultados y Discusiones#

Exploración de los datos#

Para la exploración de los datos, se utilizaron los audios originales con los cuales se generaron los espectogrmas, ya que estos nos podrian ayudar a entender las diferencias entre los cantos de las aves, y como esto puede ayudar a identificarlas.

Hide code cell source
import os
import pandas as pd
import torch
import torchaudio
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
import plotly.express as px
import librosa
import librosa.display
import IPython.display as ipd
import sklearn
import warnings
import seaborn as sns
warnings.filterwarnings('ignore')
Hide code cell source
#Metadata de los audios
train_csv=pd.read_csv('C:/Users/kaes1/Desktop/MachineLearningUN/Proyecto/train_metadata.csv')
train_csv.head()
primary_label secondary_labels type latitude longitude scientific_name common_name author license rating time url filename
0 afrsil1 [] ['call', 'flight call'] 12.3910 -1.4930 Euodice cantans African Silverbill Bram Piot Creative Commons Attribution-NonCommercial-Sha... 2.5 08:00 https://www.xeno-canto.org/125458 afrsil1/XC125458.ogg
1 afrsil1 ['houspa', 'redava', 'zebdov'] ['call'] 19.8801 -155.7254 Euodice cantans African Silverbill Dan Lane Creative Commons Attribution-NonCommercial-Sha... 3.5 08:30 https://www.xeno-canto.org/175522 afrsil1/XC175522.ogg
2 afrsil1 [] ['call', 'song'] 16.2901 -16.0321 Euodice cantans African Silverbill Bram Piot Creative Commons Attribution-NonCommercial-Sha... 4.0 11:30 https://www.xeno-canto.org/177993 afrsil1/XC177993.ogg
3 afrsil1 [] ['alarm call', 'call'] 17.0922 54.2958 Euodice cantans African Silverbill Oscar Campbell Creative Commons Attribution-NonCommercial-Sha... 4.0 11:00 https://www.xeno-canto.org/205893 afrsil1/XC205893.ogg
4 afrsil1 [] ['flight call'] 21.4581 -157.7252 Euodice cantans African Silverbill Ross Gallardy Creative Commons Attribution-NonCommercial-Sha... 3.0 16:30 https://www.xeno-canto.org/207431 afrsil1/XC207431.ogg

La información de la metadata del conjunto de datos, tiene información sobre el tipo de canto de la especie (llamado, canción, alerta), las coordenadas en las cuales se realizó la grabación del audio, quien fue el autor de la grabación, la duración de la grabación, entre otros datos. Esta información no será utilizada en el modelo, pero permite conocer un poco más sobre el contexto de los audios.

Hide code cell source
# Muestra de audios
base_dir = 'C:/Users/kaes1/Desktop/MachineLearningUN/Audios/train_audio'
train_csv['full_path'] = base_dir+ '/' + train_csv['filename']
# train_csv['full_path'] = base_dir+ '/' + train_csv['primary_label'] + '/' + train_csv['filename']
brnowl = train_csv[train_csv['primary_label'] == "brnowl"].sample(1, random_state = 33)['full_path'].values[0]
comsan = train_csv[train_csv['primary_label'] == 'comsan'].sample(1, random_state = 33)['full_path'].values[0]
houspa = train_csv[train_csv['primary_label'] == "houspa"].sample(1, random_state = 33)['full_path'].values[0]
mallar3 = train_csv[train_csv['primary_label'] == 'mallar3'].sample(1, random_state = 33)['full_path'].values[0]
norcar = train_csv[train_csv['primary_label'] == 'norcar'].sample(1, random_state = 33)['full_path'].values[0]
skylar = train_csv[train_csv['primary_label'] == 'skylar'].sample(1, random_state = 33)['full_path'].values[0]
birds= ["brnowl", "comsan", "houspa", "mallar3", "norcar",'skylar']
  • Muestra de audios

Para entender porque los cantos de las aves pueden ayudar en la identificación de especies, se muestran a continuación algunos ejemplos de audio obtenidos desde el repositorio de Xeno-canto.

_images/skylar.jpg

Fig. 12 Especie Alauda arvensis (skylar)#

ipd.Audio(skylar)
_images/houspa.jpg

Fig. 13 Especie Passer domesticus (houspa)#

ipd.Audio(houspa)
_images/comsan.jpg

Fig. 14 Especie Actitis hypoleucos (comsan)#

ipd.Audio(comsan)
Hide code cell source
y_brnowl, sr_brnowl = librosa.load(brnowl)
audio_brnowl, _ = librosa.effects.trim(y_brnowl)

y_comsan, sr_comsan = librosa.load(comsan)
audio_comsan, _ = librosa.effects.trim(y_comsan)

y_houspa , sr_houspa  = librosa.load(houspa)
audio_houspa , _ = librosa.effects.trim(y_houspa)

y_mallar3, sr_mallar3 = librosa.load(mallar3)
audio_mallar3, _ = librosa.effects.trim(y_mallar3)

y_norcar, sr_norcar = librosa.load(norcar)
audio_norcar, _ = librosa.effects.trim(y_norcar)

y_skylar, sr_skylar = librosa.load(skylar)
audio_skylar, _ = librosa.effects.trim(y_skylar)

Para una muestra de 6 especies, se grafican las ondas sonoras extraídas de los audios. Aqui se observa que los audios tienen diferentes duraciones, desde segundos hasta minutos, y que la amplitud y frecuencia de las ondas es diferente de una especie a otra.

Hide code cell source
# graficas de señales de audio
fig, ax = plt.subplots(6, figsize = (16, 12))
fig.suptitle('Ondas sonoras', fontsize=16)
# birds= ["brnowl", "comsan", "houspa ", "mallar3", "norcar",'skylar']
librosa.display.waveshow(y = audio_brnowl, sr = sr_brnowl, color = "#A300F9", ax=ax[0])
librosa.display.waveshow(y = audio_comsan, sr = sr_comsan, color = "#4300FF", ax=ax[1])
librosa.display.waveshow(y = audio_houspa , sr = sr_houspa , color = "#009DFF", ax=ax[2])
librosa.display.waveshow(y = audio_mallar3, sr = sr_mallar3, color = "#00FFB0", ax=ax[3])
librosa.display.waveshow(y = audio_norcar, sr = sr_norcar, color = "#D9FF00", ax=ax[4])
librosa.display.waveshow(y = audio_skylar, sr = sr_skylar, color = "r", ax=ax[5]);

for i, name in zip(range(6), birds):
    ax[i].set_ylabel(name, fontsize=13)
_images/459e464efe7397f8b0b4d7907c4826d7f7212016d9488cf3e5012518aade2e32.png
  • Distribución de los datos

Hide code cell source
ax = train_csv.groupby('primary_label').filter(lambda x: len(x) >= 100 )['primary_label'].value_counts().plot(kind='bar',
                                    figsize=(14,8),
                                    title="Número de registros por especie (100 o más registros)")
ax.set_xlabel("Especies")
ax.set_ylabel("Frequencia")
Text(0, 0.5, 'Frequencia')
_images/07076e8ed0c068fd50cea622c9a1a4f32615f31a2a225d0d475f9ff4851e8e88.png

En el gráfico de barras se visualizan aquellas especies con 100 o más muestras de espectogramas. Solo 47 de las 152 especies tienen 100 o mas espectrogramas en el conjunto de datos.

Hide code cell source
ax = train_csv.groupby('primary_label').filter(lambda x: len(x) < 100 )['primary_label'].value_counts().plot(kind='bar',
                                    figsize=(14,8),
                                    title="Número de registros por especie (menos de 100 registros)")
ax.set_xlabel("Especies")
ax.set_ylabel("Frequencia")
Text(0, 0.5, 'Frequencia')
_images/c963985d61353b404fc89937d91fb5438080a7f508bc8375af94fe5eeb57bfd0.png

Se puede observar, que la gran mayoría de especies cuentan con menos de 100 espectrogramas en el conjuntos de datos, algunas incluso tienen menos de 10 especies, lo que indica que el conjunto de datos esta considerablemente desbalanceado.

En el siguiente gráfico se puede ver el rating o puntaje que tienen los audios en el sitio web de Xeno-canto. Este puntaje indica que tan buena es la grabación del canto de la ave (puntaje dado por la comunidad). La mayoría de los audios, tienen un puntaje igual o superior a 3, por lo que se puede considerar que la muestra de datos es representativa de los cantos de las especies, y por lo tanto, aptos para ser usados en el entrenamiento del modelo (por consiguiente, los espectrogramas generados deben ser aptos tambien).

Hide code cell source
ax = train_csv.groupby('rating')['rating'].value_counts().plot(kind='bar',
                                    figsize=(14,8),
                                    title="Puntaje del audio")
ax.set_xlabel("Especies")
ax.set_ylabel("Frequencia")
Text(0, 0.5, 'Frequencia')
_images/45ad932b8f76f79cda84d761b71d9fcab8932a8595e4e768a11e2daf0919af99.png
  • Dimensión de las imágenes

Debido a que las imágenes de los espectrogramas están a color, tienen 3 canales de color (red, green, blue). Las imágenes no tienen el mismo tamaño, ya que difieren en el ancho, mientras que la altura es igual para todas. Debido a las imágenes deben tener las mismas dimensiones antes de convertirlas en matrices de datos, se deben escalar todas al mismo tamaño. Esto se realiza en la etapa de preprocesamiento previa a la implementación de modelo.

Hide code cell source
IMG_DIR = 'C:/Users/kaes1/Desktop/MachineLearningUN/Proyecto/Pruebas/'
especies_list = os.listdir(IMG_DIR)

images = os.listdir(os.path.join(IMG_DIR, especies_list[0]))
for image in images[:10]: 
    image_ = plt.imread(os.path.join(IMG_DIR, especies_list[0], image))
    print(image_.shape)        
(1025, 255, 3)
(1025, 2002, 3)
(1025, 692, 3)
(1025, 737, 3)
(1025, 1606, 3)
(1025, 359, 3)
(1025, 1307, 3)
(1025, 4772, 3)
(1025, 6568, 3)
(1025, 1005, 3)

Balanceo de datos#

Para realizar balanceo de datos en imagenes, por lo general se emplean técnicas que involucran desplazar, ampliar, reducir, girar, voltear, distorsionar o sombrear algunas de las imagenes del conjunto de entrenamiento, con el fin de añadir ruido y construir un nuevo grupo de datos [Rahman, 2023]. Acorde a la literatura, se cita que, en el caso de los espectrogramas, aún cuando son imágenes, no es adecuado aplicar este tipo de técnicas para generar nuevos datos, debido a que podrían contaminar la información contenida en la imagen. Un ejemplo de esto, es que al girar la imagen de un espectograma, ya no tendria relación con el audio real, e información como la frecuencia o la amplitud de la onda sonora de los audios, deja de ser representativa de la especie.

Por lo tanto, el balanceo de datos se tendría que realizar sobre los audios originales a partir de los cuales se generó el conjunto de imágenes de espectrogramas, o sobre los espectrogramas extraídos de los audios, antes de generar las imágenes. Para el caso de los audios, existen diferentes tipos de técnicas, como ruido Gaussiano, desplazamiento de tono y estiramiento temporal [Jahangir et al., 2022], que permiten crear datos sintenticos adecuados para robusteser los modelos de aprendizaje automatico.

Tomando uno de los audios obtenidos desde Xeno-canto, se ilustra cómo sería el proceso de balanceo de datos utilizando la técnica de añadir rudio Gaussiano (Gaussian noise) al audio, con el audio de la especie norcar (Cardinalis cardinalis). El ruido Gaussiano sigue una distribución normal (Gaussiana), el cual será introducida al audio original de la especie, considerando tambien un valor de amplitud (σ).

Hide code cell source
ipd.Audio(norcar)
Hide code cell source
fig, ax = plt.subplots(1, figsize = (10, 4))
fig.suptitle('Onda Sonora de norcar', fontsize=16)
librosa.display.waveshow(y = audio_norcar, sr = sr_norcar, color = "r")
<librosa.display.AdaptiveWaveplot at 0x21f4d188940>
_images/3bd7ffa536e6b7f989e4816ad8e43267cfd94f9c368c0959fae0a9b59a41ebff.png

A continuación, se añade rudio Gaussiano al audio, el cual ayuda a simular ruido ambiente en la grabación. Se considera una aplitud de 0.01, para que sea mas evidente el efecto del rudio Gaussiano en el audio, pero lo ideal es encontrar el valor adecuado de amplitud (σ), que permita robustecer el modelo sin afectar su desempeño.

Hide code cell source
# Añadir ruido blanco
wn = np.random.randn(len(audio_norcar))
data_wn = audio_norcar+ 0.01*wn
ipd.Audio(data_wn, rate=22050)
Hide code cell source
fig, ax = plt.subplots(1, figsize = (10, 4))
fig.suptitle('Onda Sonora de norcar + Gaussian Noise', fontsize=16)
librosa.display.waveshow(y = data_wn, sr = sr_norcar, color = "r")
<librosa.display.AdaptiveWaveplot at 0x21f539208b0>
_images/7ff4a81ce99b7c4a1822df1cc5dc3b6d433c634d7be06ae976676208785728c3.png

Se evidencia el cambio en la onda sonora del audio al introducir ruido Gaussiano en la muestra original.

Para el desarrollo del presente trabajo, no fue posible la implementación de técnicas de balanceo, debido a que el conjunto de datos a utilizar, ya son imagenes preprocesadas, y no los audios en sí.

Debido a que trabajar con la muestra total de datos, la cual está considerablemente desbalanceada, podria dificultar el desarrollo y desempeño de los modelos, se decide escoger un subcojunto del conjunto de datos. Este subconjunto está conformado por las 6 especies con mayor cantidad de datos, las cuales cuentan con 500 muestras de espectrogramas cada una.

Selección de métrica#

La métrica seleccionada para evaluar los modelos es AUC-ROC (Área bajo la curva ROC). Se escoge esta métrica debido a que es relevante cuando se necesita evaluar el rendimiento del modelo en problemas de clasificación multiclase con técnicas de “one-vs-all”, y permite medir la capacidad del modelo para distinguir entre clases.

Implementación de modelo#

Las especies que se van a utilizar para entrenar el modelo son: ‘brnowl’, ‘comsan’, ‘houspa’, ‘mallar3’, ‘norcar’, ‘skylar’

Previo a la implementación de los modelos, fue necesario convertir las imagenes de los espectogramas para las 6 especies en arreglos matrciales

Hide code cell source
from tensorflow.keras.preprocessing import image

especies_list = list(train_csv.groupby('primary_label').filter(lambda x: len(x) >= 480 )['primary_label'].value_counts().index)

def load_images_from_path(path, label):
    images = []
    labels = []

    for file in os.listdir(path):
        images.append(image.img_to_array(image.load_img(os.path.join(path, file), target_size=(224, 224, 3))))
        labels.append((label))
        
    return images, labels

def show_images(images):
    fig, axes = plt.subplots(1, 8, figsize=(20, 20), subplot_kw={'xticks': [], 'yticks': []})

    for i, ax in enumerate(axes.flat):
        ax.imshow(images[i] / 255)
        
x = []
y = []

En la siguiente figura se pueden visualizar los espectrogramas para las seis especies que se van a clasificar. Los espectrogramas fueron procesados y se modificaron las dimensiones de altura y ancho, para que todas las imágenes tengan las mismas dimeniones de 224 x 224.

Hide code cell source
NUEVO_DIR = 'C:/Users/kaes1/Desktop/MachineLearningUN/Proyecto/Pruebas'
label = 0
for folder in especies_list:
    images, labels = load_images_from_path(os.path.join(NUEVO_DIR, folder), label)
    show_images(images)
        
    x += images
    y += labels
    label = label + 1
_images/40f14d6a7f06c24fb07611c46076d363c86f6a63d7c800024562169cbde61bef.png _images/0eb4bc09d2bec420fd166b24b6776abe43850d1ed1ebbed4b3b585bffd8c6d30.png _images/36482d31d22947f5e88bc8d5d0a206bb6859d235d69313fb76c3c6d7b0c32f37.png _images/932fd147e60b47eae6e52cce8f70a5b0c74493fb851bb03a9f6d8c6b4503049b.png _images/564662b31516aef9ad17687ee81e34664564ab80e4110b2c82dc1879ee1466f9.png _images/deb92d9cd70ab116c4d7673d4a15ae83f5ffc685547adb912735487c65418bbb.png
Hide code cell source
from tensorflow.keras.utils import to_categorical
from sklearn.model_selection import train_test_split

x_trainval, x_test, y_trainval, y_test = train_test_split(x, y, stratify=y, test_size=0.2, random_state=0)
x_train, x_val, y_train, y_val = train_test_split(x_trainval, y_trainval, stratify=y_trainval, test_size=0.2, random_state=0)

x_train_norm = np.array(x_train) / 255
x_val_norm = np.array(x_val) / 255
x_test_norm = np.array(x_test) / 255

y_train_encoded = to_categorical(y_train)
y_val_encoded = to_categorical(y_val)
y_test_encoded = to_categorical(y_test)

N_CLASSES = len(especies_list)
N_CLASSES
6

Para la preparación de los datos, se dividió el conjuntos de datos en 3 subconjuntos: entrenamiento, validación y prueba. La división de datos se hizo de manera estratificada, asegurando que se mantenga la proporción de datos por especie en todos los subconjuntos de datos.

Tamaños de los conjuntos de datos:

  • Entrenamiento: 1920

  • Validación: 480

  • Prueba: 600

Luego de dividir los dato, se normalizan las características de entrada dividiendo cada valor de píxel por 255. Esto escala los valores de píxel en el rango [0, 1].

  • Red Neuronal Convolucional

El primer modelo entrenado, consiste en una red neuronal convolucional, cuya arquitectura se definió a través de prueba y error. Se empezó con arquitectura simple de red, la cual tenia inicialmente solo la capa de entra, una capa de convolucion, una capa de agrupamiento, una capa de regularización, la capa de aplanamiento y finalmente una capa densa que se conecta a la capa de salida. La función de perdida utilizada en este caso es categorical crossentropy, ya que nos encontramos ante un caso de asignar una unica etiqueta entre multiples categorías.

Hide code cell source
# Crear CNN model
import tensorflow as tf
from keras import regularizers

weight_decay = 1e-4
model = tf.keras.models.Sequential()
model.add(tf.keras.layers.Input(shape=(224, 224, 3)))
model.add(tf.keras.layers.Conv2D(32, 3, strides=1, padding='same', activation='relu',kernel_regularizer=regularizers.l2(weight_decay)))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
model.add(tf.keras.layers.Dropout(0.2))
model.add(tf.keras.layers.Conv2D(64, 3, padding='same', activation='relu',kernel_regularizer=regularizers.l2(weight_decay)))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
model.add(tf.keras.layers.Dropout(0.3))
model.add(tf.keras.layers.Conv2D(128, 3, padding='same', activation='relu',kernel_regularizer=regularizers.l2(weight_decay)))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
model.add(tf.keras.layers.Dropout(0.4))
model.add(tf.keras.layers.Flatten())
model.add(tf.keras.layers.Dense(256, activation='relu'))
model.add(tf.keras.layers.Dense(N_CLASSES, activation='softmax'))

# Compilar modelo
model.compile(
    loss='categorical_crossentropy',
    optimizer=tf.keras.optimizers.Adam(),
    metrics=[tf.keras.metrics.AUC(),'accuracy'],
)
Hide code cell source
from keras.utils.vis_utils import plot_model
plot_model(model, to_file='model_plot.png', show_shapes=True, show_layer_names=True)
_images/42b9d160d1e00136e9e720ea95731343988f61803fbe35eaa1ed42122764544c.png

En la arquitectura planteada, se tiene una secuencia de varias capas convolucionales para extraer características de las imágenes, capas de agrupación para reducir la resolución, capas de dropout para reducir el sobreajuste y capas densas para la clasificación final. El regularizador L2 se utiliza para controlar el sobreajuste.

Hide code cell source
# Entrenar modelo por 10 epocas, capturar history
# model.load_weights('C:/Users/kaes1/Desktop/MachineLearningUN/cnn_model1.h5')
history = model.fit(x_train_norm, y_train_encoded, epochs=10, validation_data=(x_val_norm, y_val_encoded), batch_size=32)
Hide code cell output
Epoch 1/10
60/60 [==============================] - 360s 6s/step - loss: 3.0611 - auc_1: 0.4897 - accuracy: 0.1531 - val_loss: 1.8039 - val_auc_1: 0.5000 - val_accuracy: 0.1667
Epoch 2/10
60/60 [==============================] - 376s 6s/step - loss: 1.7917 - auc_1: 0.5475 - accuracy: 0.2104 - val_loss: 1.6912 - val_auc_1: 0.7064 - val_accuracy: 0.3000
Epoch 3/10
60/60 [==============================] - 343s 6s/step - loss: 1.5448 - auc_1: 0.7542 - accuracy: 0.3792 - val_loss: 1.3594 - val_auc_1: 0.8316 - val_accuracy: 0.5063
Epoch 4/10
60/60 [==============================] - 181s 3s/step - loss: 1.3251 - auc_1: 0.8268 - accuracy: 0.4995 - val_loss: 1.2377 - val_auc_1: 0.8693 - val_accuracy: 0.5750
Epoch 5/10
60/60 [==============================] - 91s 2s/step - loss: 1.1642 - auc_1: 0.8699 - accuracy: 0.5594 - val_loss: 0.9244 - val_auc_1: 0.9224 - val_accuracy: 0.6687
Epoch 6/10
60/60 [==============================] - 93s 2s/step - loss: 0.9052 - auc_1: 0.9210 - accuracy: 0.6562 - val_loss: 0.8645 - val_auc_1: 0.9316 - val_accuracy: 0.6750
Epoch 7/10
60/60 [==============================] - 97s 2s/step - loss: 0.7265 - auc_1: 0.9489 - accuracy: 0.7411 - val_loss: 0.7560 - val_auc_1: 0.9448 - val_accuracy: 0.7333
Epoch 8/10
60/60 [==============================] - 345s 6s/step - loss: 0.6077 - auc_1: 0.9641 - accuracy: 0.7745 - val_loss: 0.7668 - val_auc_1: 0.9427 - val_accuracy: 0.7437
Epoch 9/10
60/60 [==============================] - 96s 2s/step - loss: 0.5077 - auc_1: 0.9746 - accuracy: 0.8203 - val_loss: 0.7666 - val_auc_1: 0.9456 - val_accuracy: 0.7417
Epoch 10/10
60/60 [==============================] - 99s 2s/step - loss: 0.4131 - auc_1: 0.9835 - accuracy: 0.8516 - val_loss: 0.7689 - val_auc_1: 0.9470 - val_accuracy: 0.7667

Al observar los valores de AUC obtenidos al entrenar la red, se puede ver que el modelo si es bueno para disernir una especie de la otra. La red entrenada logra su mejor desempeño de AUC en la epoca 7, donde las metricas de entrenamiento no distan mucho de las métricas de validación.

acc = history.history['auc_1']
val_acc = history.history['val_auc_1']
epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, '-', label='Training AUC')
plt.plot(epochs, val_acc, ':', label='Validation AUC')
plt.title('Training and Validation AUC')
plt.xlabel('Epoch')
plt.ylabel('AUC')
plt.legend(loc='lower right')
plt.plot()
[]
_images/97de05955912c6825b54667ca85ee32aa635cd9b897713e18a25f1d8b0a7f7d3.png
Hide code cell source
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, '-', label='Training Accuracy')
plt.plot(epochs, val_acc, ':', label='Validation Accuracy')
plt.title('Training and Validation Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.plot()
[]
_images/b39c6715f02862fa86d8259b391cae6d1bcd501322f1ce19cbc020c621d8f8cc.png
Hide code cell source
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)

plt.plot(epochs, loss, '-', label='Training Loss')
plt.plot(epochs, val_loss, ':', label='Validation Loss')
plt.title('Training and Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend(loc='lower right')
plt.plot()
[]
_images/ca8a68ad6fb7f7af4e87300272e72f74ad78eb872c433c5f54e42124609cfeda.png

Ahora se valida que tan bueno es el modelo para identificar las especies en datos que no ha visto nunca. Al obtener realizar la predicción con los datos de prueba y trazar la matriz de confusión, se detalla que la red entrenada si logra identificar bastante bien cada especie, aunque parece tener falencias para identificar correctamente dos especies: bornowl y skylar.

Hide code cell source
from sklearn.metrics import confusion_matrix
import seaborn as sns
sns.set()

y_predicted = model.predict(x_test_norm)
mat = confusion_matrix(y_test_encoded.argmax(axis=1), y_predicted.argmax(axis=1))
class_labels = especies_list

sns.heatmap(mat, square=True, annot=True, fmt='d', cbar=False, cmap='Blues',
            xticklabels=class_labels,
            yticklabels=class_labels)

plt.xlabel('Predicted label')
plt.ylabel('Actual label')
19/19 [==============================] - 5s 211ms/step
Text(110.44999999999997, 0.5, 'Actual label')
_images/43647739177e1f396f8ae239abf79b42a2eff52d11c6d987e6f99528cb847bb5.png
Hide code cell source
# Compute the final loss and accuracy
final_loss, final_auc,final_acc = model.evaluate(x_test_norm, y_test_encoded, verbose=0)
print("Final loss: {0:.6f}, Final AUC-ROC: {1:.6f}, final accuracy: {2:.6f}".format(final_loss, final_auc, final_acc))
Final loss: 0.845954, Final AUC-ROC: 0.936800, final accuracy: 0.725000

El valor de AUC final para este modelo es bastante bueno, siendo un valor de 0.937.

En la siguiente gráfica se puede observar las predicciones frente a las etiquetas reales de los espectrogramas.

Hide code cell source
figure = plt.figure(figsize=(20, 8))
for i, index in enumerate(np.array(range(0,12,1))):
    ax = figure.add_subplot(4, 6, i + 1, xticks=[], yticks=[])
    ax.imshow(np.squeeze(x_test[index]/255))
    predict_index = class_labels[(np.argmax(y_predicted[index]))]
    true_index = class_labels[(np.argmax(y_test_encoded[index]))]
    
    ax.set_title("{} ({})".format((predict_index), 
                                  (true_index)),
                                  color=("green" if predict_index == true_index else "red"))
_images/02ef4f51a9f2e0e8b38605029b85cb760cd69a56a57729f558e3b1f2c29d3f60.png

La red anterior, podría ser mejorada al hiperparametrizar algunos de los parámetros, ya que los parámetros actuales de la red fueron establecidos por prueba y error. Para este caso, se va hiperparametrizar el ratio de dropout de las capas de la red, y tambien se van a evaluar diferntes tamaños de lotes (batch) para los datos de entrada a la red.

Hide code cell source
import numpy as np
import pandas as pd

from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

# keras import

import tensorflow as tf
from keras.models import Sequential
from keras.layers import Dense, Dropout, Conv2D, MaxPooling2D, AveragePooling2D, Flatten
from keras.utils import to_categorical
from keras.callbacks import EarlyStopping
from keras import regularizers

# hyperparameter optimization
from sklearn.model_selection import GridSearchCV
from keras.wrappers.scikit_learn import KerasClassifier

# data augmentation
from keras.preprocessing.image import ImageDataGenerator

# visualisation
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import seaborn as sns
#set figure size
plt.rcParams['figure.figsize'] = 12, 6
sns.set_style('white')

# others
from random import randrange
from time import time
Hide code cell source
start=time()

weight_decay = 1e-4
n_epochs_cv = 10 
n_cv = 3
# se define la funcion para crear el modelo
# se va tratar de optimizar el ratio del dropout
def create_mlp_model(dropout_rate=0):
    # crear modelo
    model = tf.keras.models.Sequential()
    model.add(tf.keras.layers.Input(shape=(224, 224, 3)))
    model.add(tf.keras.layers.Conv2D(32, 3, strides=1, padding='same', activation='relu',kernel_regularizer=regularizers.l2(weight_decay)))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    # añadir capa de dropout si el ratio no es nula
    if dropout_rate != 0:
        model.add(Dropout(rate=dropout_rate)) 
              
    model.add(tf.keras.layers.Conv2D(64, 3, padding='same', activation='relu',kernel_regularizer=regularizers.l2(weight_decay)))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    # añadir capa de dropout si el ratio no es nula   
    if dropout_rate != 0:
        model.add(Dropout(rate=dropout_rate))   
    model.add(tf.keras.layers.Conv2D(128, 3, padding='same', activation='relu',kernel_regularizer=regularizers.l2(weight_decay)))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    if dropout_rate != 0:
        model.add(Dropout(rate=dropout_rate)) 
    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(256, activation='relu'))
    model.add(tf.keras.layers.Dense(N_CLASSES, activation='softmax'))
 
    
    # Compile model
    model.compile( 
        optimizer='adam',
        loss='categorical_crossentropy',
        metrics=[tf.keras.metrics.AUC()],
        )    
    return model

# función para mostrar los resultados del grid search
def display_cv_results(search_results):
    print('Best score = {:.4f} using {}'.format(search_results.best_score_, search_results.best_params_))
    means = search_results.cv_results_['mean_test_score']
    stds = search_results.cv_results_['std_test_score']
    params = search_results.cv_results_['params']
    for mean, stdev, param in zip(means, stds, params):
        print('mean test AUC +/- std = {:.4f} +/- {:.4f} with: {}'.format(mean, stdev, param))    
    
# ccrear modelo
model = KerasClassifier(build_fn=create_mlp_model, verbose=1)
# diccionario de parámetros
param_grid = {
    'batch_size': [16, 32, 64],
    'epochs': [n_epochs_cv],
    'dropout_rate': [0.0, 0.10, 0.20, 0.30, 0.4],
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=n_cv)
grid_result = grid.fit(x_train_norm, y_train_encoded)  # ajustar todos los datos

# resultados
print('time for grid search = {:.0f} sec'.format(time()-start))
display_cv_results(grid_result)
Hide code cell output
---------------------------------------------------------------------------
_RemoteTraceback                          Traceback (most recent call last)
_RemoteTraceback: 
"""
Traceback (most recent call last):
  File "c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\joblib\externals\loky\process_executor.py", line 463, in _process_worker
    r = call_item()
  File "c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\joblib\externals\loky\process_executor.py", line 291, in __call__
    return self.fn(*self.args, **self.kwargs)
  File "c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py", line 589, in __call__
    return [func(*args, **kwargs)
  File "c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py", line 589, in <listcomp>
    return [func(*args, **kwargs)
  File "c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\sklearn\utils\parallel.py", line 127, in __call__
    return self.function(*args, **kwargs)
  File "c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\sklearn\model_selection\_validation.py", line 724, in _fit_and_score
    X_train, y_train = _safe_split(estimator, X, y, train)
  File "c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\sklearn\utils\metaestimators.py", line 155, in _safe_split
    X_subset = _safe_indexing(X, indices)
  File "c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\sklearn\utils\__init__.py", line 355, in _safe_indexing
    return _array_indexing(X, indices, indices_dtype, axis=axis)
  File "c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\sklearn\utils\__init__.py", line 184, in _array_indexing
    return array[key] if axis == 0 else array[:, key]
  File "c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\numpy\core\memmap.py", line 334, in __getitem__
    res = super().__getitem__(index)
numpy.core._exceptions._ArrayMemoryError: Unable to allocate 735. MiB for an array with shape (1280, 224, 224, 3) and data type float32
"""

The above exception was the direct cause of the following exception:

MemoryError                               Traceback (most recent call last)
c:\Users\kaes1\Desktop\MachineLearningUN\proyectofinal\notebooks.ipynb Cell 112 line 5
     <a href='vscode-notebook-cell:/c%3A/Users/kaes1/Desktop/MachineLearningUN/proyectofinal/notebooks.ipynb#Y262sZmlsZQ%3D%3D?line=51'>52</a> param_grid = {
     <a href='vscode-notebook-cell:/c%3A/Users/kaes1/Desktop/MachineLearningUN/proyectofinal/notebooks.ipynb#Y262sZmlsZQ%3D%3D?line=52'>53</a>     'batch_size': [16, 32, 64],
     <a href='vscode-notebook-cell:/c%3A/Users/kaes1/Desktop/MachineLearningUN/proyectofinal/notebooks.ipynb#Y262sZmlsZQ%3D%3D?line=53'>54</a>     'epochs': [n_epochs_cv],
     <a href='vscode-notebook-cell:/c%3A/Users/kaes1/Desktop/MachineLearningUN/proyectofinal/notebooks.ipynb#Y262sZmlsZQ%3D%3D?line=54'>55</a>     'dropout_rate': [0.0, 0.10, 0.20, 0.30, 0.4],
     <a href='vscode-notebook-cell:/c%3A/Users/kaes1/Desktop/MachineLearningUN/proyectofinal/notebooks.ipynb#Y262sZmlsZQ%3D%3D?line=55'>56</a> }
     <a href='vscode-notebook-cell:/c%3A/Users/kaes1/Desktop/MachineLearningUN/proyectofinal/notebooks.ipynb#Y262sZmlsZQ%3D%3D?line=56'>57</a> grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=n_cv)
---> <a href='vscode-notebook-cell:/c%3A/Users/kaes1/Desktop/MachineLearningUN/proyectofinal/notebooks.ipynb#Y262sZmlsZQ%3D%3D?line=57'>58</a> grid_result = grid.fit(x_train_norm, y_train_encoded)  # fit the full dataset as we are using cross validation 
     <a href='vscode-notebook-cell:/c%3A/Users/kaes1/Desktop/MachineLearningUN/proyectofinal/notebooks.ipynb#Y262sZmlsZQ%3D%3D?line=59'>60</a> # print out results
     <a href='vscode-notebook-cell:/c%3A/Users/kaes1/Desktop/MachineLearningUN/proyectofinal/notebooks.ipynb#Y262sZmlsZQ%3D%3D?line=60'>61</a> print('time for grid search = {:.0f} sec'.format(time()-start))

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\sklearn\base.py:1151, in _fit_context.<locals>.decorator.<locals>.wrapper(estimator, *args, **kwargs)
   1144     estimator._validate_params()
   1146 with config_context(
   1147     skip_parameter_validation=(
   1148         prefer_skip_nested_validation or global_skip_validation
   1149     )
   1150 ):
-> 1151     return fit_method(estimator, *args, **kwargs)

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\sklearn\model_selection\_search.py:898, in BaseSearchCV.fit(self, X, y, groups, **fit_params)
    892     results = self._format_results(
    893         all_candidate_params, n_splits, all_out, all_more_results
    894     )
    896     return results
--> 898 self._run_search(evaluate_candidates)
    900 # multimetric is determined here because in the case of a callable
    901 # self.scoring the return type is only known after calling
    902 first_test_score = all_out[0]["test_scores"]

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\sklearn\model_selection\_search.py:1419, in GridSearchCV._run_search(self, evaluate_candidates)
   1417 def _run_search(self, evaluate_candidates):
   1418     """Search all candidates in param_grid"""
-> 1419     evaluate_candidates(ParameterGrid(self.param_grid))

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\sklearn\model_selection\_search.py:845, in BaseSearchCV.fit.<locals>.evaluate_candidates(candidate_params, cv, more_results)
    837 if self.verbose > 0:
    838     print(
    839         "Fitting {0} folds for each of {1} candidates,"
    840         " totalling {2} fits".format(
    841             n_splits, n_candidates, n_candidates * n_splits
    842         )
    843     )
--> 845 out = parallel(
    846     delayed(_fit_and_score)(
    847         clone(base_estimator),
    848         X,
    849         y,
    850         train=train,
    851         test=test,
    852         parameters=parameters,
    853         split_progress=(split_idx, n_splits),
    854         candidate_progress=(cand_idx, n_candidates),
    855         **fit_and_score_kwargs,
    856     )
    857     for (cand_idx, parameters), (split_idx, (train, test)) in product(
    858         enumerate(candidate_params), enumerate(cv.split(X, y, groups))
    859     )
    860 )
    862 if len(out) < 1:
    863     raise ValueError(
    864         "No fits were performed. "
    865         "Was the CV iterator empty? "
    866         "Were there no candidates?"
    867     )

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\sklearn\utils\parallel.py:65, in Parallel.__call__(self, iterable)
     60 config = get_config()
     61 iterable_with_config = (
     62     (_with_config(delayed_func, config), args, kwargs)
     63     for delayed_func, args, kwargs in iterable
     64 )
---> 65 return super().__call__(iterable_with_config)

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py:1952, in Parallel.__call__(self, iterable)
   1946 # The first item from the output is blank, but it makes the interpreter
   1947 # progress until it enters the Try/Except block of the generator and
   1948 # reach the first `yield` statement. This starts the aynchronous
   1949 # dispatch of the tasks to the workers.
   1950 next(output)
-> 1952 return output if self.return_generator else list(output)

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py:1595, in Parallel._get_outputs(self, iterator, pre_dispatch)
   1592     yield
   1594     with self._backend.retrieval_context():
-> 1595         yield from self._retrieve()
   1597 except GeneratorExit:
   1598     # The generator has been garbage collected before being fully
   1599     # consumed. This aborts the remaining tasks if possible and warn
   1600     # the user if necessary.
   1601     self._exception = True

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py:1699, in Parallel._retrieve(self)
   1692 while self._wait_retrieval():
   1693 
   1694     # If the callback thread of a worker has signaled that its task
   1695     # triggered an exception, or if the retrieval loop has raised an
   1696     # exception (e.g. `GeneratorExit`), exit the loop and surface the
   1697     # worker traceback.
   1698     if self._aborting:
-> 1699         self._raise_error_fast()
   1700         break
   1702     # If the next job is not ready for retrieval yet, we just wait for
   1703     # async callbacks to progress.

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py:1734, in Parallel._raise_error_fast(self)
   1730 # If this error job exists, immediatly raise the error by
   1731 # calling get_result. This job might not exists if abort has been
   1732 # called directly or if the generator is gc'ed.
   1733 if error_job is not None:
-> 1734     error_job.get_result(self.timeout)

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py:736, in BatchCompletionCallBack.get_result(self, timeout)
    730 backend = self.parallel._backend
    732 if backend.supports_retrieve_callback:
    733     # We assume that the result has already been retrieved by the
    734     # callback thread, and is stored internally. It's just waiting to
    735     # be returned.
--> 736     return self._return_or_raise()
    738 # For other backends, the main thread needs to run the retrieval step.
    739 try:

File c:\Users\kaes1\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py:754, in BatchCompletionCallBack._return_or_raise(self)
    752 try:
    753     if self.status == TASK_ERROR:
--> 754         raise self._result
    755     return self._result
    756 finally:

MemoryError: Unable to allocate 735. MiB for an array with shape (1280, 224, 224, 3) and data type float32
  • Aprendizaje por transferencia con MobileNetV2

El siguiente modelo a evaluar consiste en implementar el aprendizaje por transferencia a través del modelo de CNN MobileNetV2. MobileNetV2 es una red neuronal convolucional on 53 capas de profundidad, la cual está preentrenada y optimizada para su uso en dispositivos móviles, diseñada para extraer características de las imágenes de espectrogramas. Se utiizó este modelo principalmente por ser ligero para su implementación en dispositivos sin GPU, y por los buenos resultados que ha tenido en otras aplicaciones.

Hide code cell source
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras.applications.mobilenet import preprocess_input

base_model = MobileNetV2(weights='imagenet', include_top=False, input_shape=(224, 224, 3))

x_train_norm_ = preprocess_input(np.array(x_train))
x_test_norm_ = preprocess_input(np.array(x_test))
x_val_norm_ = preprocess_input(np.array(x_val))

train_features = base_model.predict(x_train_norm_)
test_features = base_model.predict(x_test_norm_)
val_features = base_model.predict(x_val_norm_)
60/60 [==============================] - 39s 591ms/step
19/19 [==============================] - 11s 524ms/step
15/15 [==============================] - 8s 540ms/step

La aquitectura planteado para el aprendizaje por transferencia es la siguiente:

Hide code cell source
model2 = tf.keras.models.Sequential()
model2.add(tf.keras.layers.Flatten(input_shape=train_features.shape[1:]))
model2.add(tf.keras.layers.Dense(1024, activation='relu'))
model2.add(tf.keras.layers.Dense(N_CLASSES, activation='softmax'))
# model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])

# Compile model
model2.compile(
    loss='categorical_crossentropy',
    optimizer=tf.keras.optimizers.Adam(),
    metrics=[tf.keras.metrics.AUC(),'accuracy'],
)

from keras.utils.vis_utils import plot_model
plot_model(model2, to_file='model_plot.png', show_shapes=True, show_layer_names=True)
_images/f244761110043945d229303c780bae3b8ef19f7ac85d25f7c0748638e245e0bd.png

En resumen, esta arquitectura toma un vector de características de entrada, lo aplana, lo pasa a través de una capa completamente conectada con activación ReLU, y luego lo conecta a la capa de salida con activación softmax para producir las probabilidades de pertenencia a cada clase en un problema de clasificación. El vector de características de entrada para este caso, fue extraido por medio de la trasnferencia de conocimiento con el modelo MobileNetV2.

Hide code cell source
hist = model2.fit(train_features, y_train_encoded, validation_data=(val_features, y_val_encoded), batch_size=32, epochs=10)
Hide code cell output
Epoch 1/10
60/60 [==============================] - 16s 222ms/step - loss: 13.9871 - auc_1: 0.7944 - accuracy: 0.5734 - val_loss: 1.4756 - val_auc_1: 0.9092 - val_accuracy: 0.7021
Epoch 2/10
60/60 [==============================] - 13s 219ms/step - loss: 0.5384 - auc_1: 0.9712 - accuracy: 0.8490 - val_loss: 0.9175 - val_auc_1: 0.9409 - val_accuracy: 0.7750
Epoch 3/10
60/60 [==============================] - 13s 215ms/step - loss: 0.1164 - auc_1: 0.9980 - accuracy: 0.9641 - val_loss: 0.8243 - val_auc_1: 0.9525 - val_accuracy: 0.7854
Epoch 4/10
60/60 [==============================] - 13s 213ms/step - loss: 0.0324 - auc_1: 0.9999 - accuracy: 0.9943 - val_loss: 0.7927 - val_auc_1: 0.9543 - val_accuracy: 0.7958
Epoch 5/10
60/60 [==============================] - 12s 208ms/step - loss: 0.0082 - auc_1: 1.0000 - accuracy: 1.0000 - val_loss: 0.7749 - val_auc_1: 0.9568 - val_accuracy: 0.7979
Epoch 6/10
60/60 [==============================] - 13s 209ms/step - loss: 0.0037 - auc_1: 1.0000 - accuracy: 1.0000 - val_loss: 0.7521 - val_auc_1: 0.9573 - val_accuracy: 0.8188
Epoch 7/10
60/60 [==============================] - 13s 211ms/step - loss: 0.0026 - auc_1: 1.0000 - accuracy: 1.0000 - val_loss: 0.7627 - val_auc_1: 0.9587 - val_accuracy: 0.8146
Epoch 8/10
60/60 [==============================] - 12s 208ms/step - loss: 0.0021 - auc_1: 1.0000 - accuracy: 1.0000 - val_loss: 0.7763 - val_auc_1: 0.9566 - val_accuracy: 0.8125
Epoch 9/10
60/60 [==============================] - 13s 212ms/step - loss: 0.0018 - auc_1: 1.0000 - accuracy: 1.0000 - val_loss: 0.7864 - val_auc_1: 0.9556 - val_accuracy: 0.8146
Epoch 10/10
60/60 [==============================] - 13s 209ms/step - loss: 0.0016 - auc_1: 1.0000 - accuracy: 1.0000 - val_loss: 0.7963 - val_auc_1: 0.9559 - val_accuracy: 0.8125

Al observar los resultados de AUC para este modelo, se tienen valores que indican overfitting durante el entrenamiento. A partir de la epoca 3, el modelo dejó de aprender, y se memorizó los datos de entrenamiento, por lo que no deberia ser capaz de generalizr bien en datos nuevos, nunca antes vistos por el modelo.

acc = hist.history['auc_1']
val_acc = hist.history['val_auc_1']
epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, '-', label='Training AUC')
plt.plot(epochs, val_acc, ':', label='Validation AUC')
plt.title('Training and Validation AUC')
plt.xlabel('Epoch')
plt.ylabel('AUC')
plt.legend(loc='lower right')
plt.plot()
[]
_images/d17df6cb64606dc86ecb3eeefbaba230054ba49286333ddc56b83db90529109c.png
Hide code cell source
acc = hist.history['accuracy']
val_acc = hist.history['val_accuracy']
epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, '-', label='Training Accuracy')
plt.plot(epochs, val_acc, ':', label='Validation Accuracy')
plt.title('Training and Validation Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.plot()
[]
_images/f8bc94c263c0aa778bd8034ddf1fb88c4b5d4a2a629de26051d7e355114ac1b8.png
Hide code cell source
loss = hist.history['loss']
val_loss = hist.history['val_loss']
epochs = range(1, len(acc) + 1)

plt.plot(epochs, loss, '-', label='Training Loss')
plt.plot(epochs, val_loss, ':', label='Validation Loss')
plt.title('Training and Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend(loc='lower right')
plt.plot()
[]
_images/9d5a8b5700f9f582192af658528c9828b897383541527ff18fe88d3051151be1.png

Al realizar las predicciones con los datos de prueba, el modelo es capaz de identificar muy bien las 6 especies de aves. Sin embargo, debido a que es un modelo con posible sobreajuste en el entrenamiento, su rendimiento podria decaer a medida que se introducen nuevos datos, con mayor rudio en la información, ya que el modelo no estaria en capacidad de generalizar.

Hide code cell source
from sklearn.metrics import confusion_matrix
import seaborn as sns
sns.set()

y_predicted2 = model2.predict(test_features)
mat = confusion_matrix(y_test_encoded.argmax(axis=1), y_predicted2.argmax(axis=1))
class_labels = especies_list

sns.heatmap(mat, square=True, annot=True, fmt='d', cbar=False, cmap='Blues',
            xticklabels=class_labels,
            yticklabels=class_labels)

plt.xlabel('Predicted label')
plt.ylabel('Actual label')
19/19 [==============================] - 1s 56ms/step
Text(351.25, 0.5, 'Actual label')
_images/bc17a28eae7683983c627851ba3bbdf8ce003a610fb6e51c5e490509ecf62f76.png
Hide code cell source
# Compute the final loss and accuracy
final_loss, final_auc,final_acc = model2.evaluate(test_features, y_test_encoded, verbose=0)
print("Final loss: {0:.6f}, Final AUC-ROC: {1:.6f}, final accuracy: {2:.6f}".format(final_loss, final_auc, final_acc))
Final loss: 0.896269, Final AUC-ROC: 0.950590, final accuracy: 0.801667

El AUC final del modelo entrenado, hasta ahora es mayor a la primera red que se entrenó, pero se considera que no es muy adecuado utilizar este modelo para predecir, por el sobreajuste que esta presentando.

A continuación se pueden ver algunos ejemplos de espectrogramas, su etiqueta real y la predicción realizada por el modelo.

figure = plt.figure(figsize=(20, 8))
for i, index in enumerate(np.array(range(0,12,1))):
    ax = figure.add_subplot(4, 6, i + 1, xticks=[], yticks=[])
    ax.imshow(np.squeeze(x_test[index]/255))
    predict_index = class_labels[(np.argmax(y_predicted2[index]))]
    true_index = class_labels[(np.argmax(y_test_encoded[index]))]
    
    ax.set_title("{} ({})".format((predict_index), 
                                  (true_index)),
                                  color=("green" if predict_index == true_index else "red"))
_images/625cb82fdfde901fe4445bf44dd194620ac1dd1bce53aff8ece726e0fc775e96.png

Conclusiones#

Los resultados obtenidos en términos de clasificación fueron prometedores y mostraron un alto nivel de precisión en la identificación de especies de aves. Esto sugiere que las CNNs son una herramienta efectiva para esta tarea específica.

Uno de los desafíos identificados fue el balanceo de datos. Dado que se contaban con espectrogramas ya procesados, modificarlos podría haber perjudicado la información importante para la clasificación. Esto resalta la importancia de tener un conjunto de datos bien equilibrado para entrenar modelos de aprendizaje automático.

El preprocesamiento adecuado de los espectrogramas, incluyendo la normalización y la segmentación adecuada, fue crucial para el éxito del modelo. Garantizar que los datos de entrada sean adecuados es esencial para el rendimiento de la CNN.

Posibles Aplicaciones Futuras y Mejoras Potenciales:#

Además de la clasificación de especies de aves, existen oportunidades para mejorar aún más estos modelos y ampliar sus aplicaciones:

Modelos Híbridos: Se pueden explorar modelos híbridos que combinan el poder de las redes neuronales convolucionales (CNNs) con otras técnicas de aprendizaje automático, como Random Forests o Support Vector Machines (SVMs). La combinación de enfoques puede ayudar a mejorar la robustez y la precisión de la clasificación.

Optimización de Hiperparámetros con Algoritmos Genéticos: La hiperparametrización es un aspecto crítico en el desarrollo de modelos de aprendizaje automático. La optimización de hiperparámetros con algoritmos genéticos u otras técnicas de búsqueda puede permitir la selección de la mejor configuración de hiperparámetros para mejorar el rendimiento del modelo.

Ampliación a Nuevas Especies y Entornos: Estos modelos pueden extenderse para incluir una gama más amplia de especies de aves y entornos. Esto podría ser valioso en la monitorización de la biodiversidad y la conservación en diferentes regiones geográficas.

Limitaciones y consideraciones#

Durante el desarrollo del proyecto se encontraron las siguientes limitantes:

  • Las imágenes de los espectrogramas utilizadas originalmente estaban en formato .TIFF, lo que inicialmente impidió la ejecución de los modelos debido a problemas de compatibilidad. Para resolver esto, se realizó una conversión de formato a .JPEG. Sin embargo, no se puede afirmar con certeza si este cambio afectó la estructura de las imágenes y, por ende, las características extraídas por los modelos.

  • Se intentó crear el conjunto de datos de espectrogramas desde cero utilizando los audios de Xeno-canto y aplicando una transformada de Fourier de 2048. Sin embargo, los resultados obtenidos con los espectrogramas generados no fueron satisfactorios, y algunas imágenes parecían haber perdido información durante el proceso de transformación.

  • Se podría mejorar el rendimiento de los modelos y ampliar su alcance trabajando directamente con los audios de las aves. Esto permitiría un mayor control sobre el procesamiento de las imágenes y la aplicación de técnicas adecuadas de balanceo de datos. De esta manera, se aseguraría que los datos de entrada para los modelos sean válidos, confiables y representativos de la población de aves de interés.